package be.dch.csv;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;

import be.dch.csv.mapper.FieldSetMapper;
import be.dch.csv.processor.AbstractCellProcessor;

/**
 * 
 * @author EH053
 * 
 * @param <T>
 */
@SuppressWarnings("hiding")
public class CSVParser<T> {
    /** * */
    public static final int FIELD_ACCESSOR = 0;
    /** * */
    public static final int METHOD_ACCESSOR = 1;
    /** * */
    public static final int CUSTOM_ACCESSOR = 2;
    /** * */
    public static final String DEFAULT_DELIMITER = ",";
    private static final Logger LOG = Logger.getLogger(CSVParser.class);
    private static final String CSV_REGEX = "(?=([^\"]*\"[^\"]*\")*[^\"]*$)";

    private transient final String delimitor;
    private transient final int accessingMethod;
    private transient final Set<AbstractCellProcessor> processors;
    private transient final Class<T> clazz;
    private transient String[] headers;
    private transient final BufferedReader reader;
    private transient FieldSetMapper<T> mapper;
    private transient final Map<String, Object> fieldSet = new HashMap<String, Object>();

    /**
     * 
     * @param clazz
     * @param inputStream
     * @param processors
     */
    public CSVParser(final Class<T> clazz, final InputStream inputStream, final Set<AbstractCellProcessor> processors) {
	this.reader = new BufferedReader(new InputStreamReader(inputStream));
	this.delimitor = CSVParser.DEFAULT_DELIMITER;
	this.accessingMethod = CSVParser.FIELD_ACCESSOR;
	this.processors = processors;
	this.clazz = clazz;
    }

    /**
     * 
     * @param clazz
     * @param inputStream
     * @param delimitor
     * @param processors
     */
    public CSVParser(final Class<T> clazz, final InputStream inputStream, final String delimitor, final Set<AbstractCellProcessor> processors) {
	this.reader = new BufferedReader(new InputStreamReader(inputStream));
	this.delimitor = delimitor;
	this.accessingMethod = CSVParser.FIELD_ACCESSOR;
	this.processors = processors;
	this.clazz = clazz;
    }

    /**
     * 
     * @param clazz
     * @param inputStream
     * @param delimitor
     * @param processors
     */
    public CSVParser(final Class<T> clazz, final InputStream inputStream, final String delimitor, final int accessingMethod, final Set<AbstractCellProcessor> processors) {
	this.reader = new BufferedReader(new InputStreamReader(inputStream));
	this.delimitor = delimitor;
	this.accessingMethod = accessingMethod;
	this.processors = processors;
	this.clazz = clazz;
    }

    /**
     * 
     * @param clazz
     * @param inputStream
     * @param delimitor
     * @param processors
     */
    public CSVParser(final Class<T> clazz, final InputStream inputStream, final String delimitor, final FieldSetMapper<T> mapper, final Set<AbstractCellProcessor> processors) {
	this.reader = new BufferedReader(new InputStreamReader(inputStream));
	this.delimitor = delimitor;
	this.accessingMethod = CSVParser.CUSTOM_ACCESSOR;
	this.mapper = mapper;
	this.processors = processors;
	this.clazz = clazz;
    }

    /**
     * 
     * @return
     */
    public <T> T readLine() {
	return this.readLine(this.getHeaders());
    }

    /**
     * 
     * @param headers
     * @return
     */
    public <T> T readLine(final String[] headers) {
	T resultBean = null;
	try {
	    final String line = this.reader.readLine();
	    if (line != null) {
		final String[] lineArray = line.split(this.delimitor + CSVParser.CSV_REGEX);

		resultBean = this.initClass();
		this.processLine(headers, resultBean, lineArray);

		if (CSVParser.CUSTOM_ACCESSOR == this.accessingMethod) {
		    resultBean = this.invokeCustomAccessor();
		}
	    }
	} catch (final InstantiationException e) {
	    CSVParser.LOG.error(e.getMessage());
	} catch (final IOException e) {
	    CSVParser.LOG.error(e.getMessage());
	} catch (final IllegalAccessException e) {
	    CSVParser.LOG.error(e.getMessage());
	}
	return resultBean;
    }

    @SuppressWarnings("unchecked")
    private <T> T invokeCustomAccessor() {
	T result = null;
	if (this.mapper == null) {
	    CSVParser.LOG.error("No mapping is defined!");
	} else {
	    result = (T) this.mapper.mapLine(this.fieldSet);
	}

	return result;
    }

    @SuppressWarnings("unchecked")
    private <T> T initClass() throws InstantiationException, IllegalAccessException {
	T result = null;
	if (this.clazz.isInterface()) {
	    CSVParser.LOG.warn("Interface is not supported!");
	} else if (this.accessingMethod != CSVParser.CUSTOM_ACCESSOR) {
	    result = (T) this.clazz.newInstance();
	}
	return result;
    }

    @SuppressWarnings("unchecked")
    private <T> void processLine(final String[] headers, final T resultBean, final String[] lineArray) {
	for (int i = 0; i < lineArray.length; i++) {
	    final AbstractCellProcessor processor = (AbstractCellProcessor) Arrays.asList(this.processors).get(i);
	    final Object result = processor.parse(lineArray[i]);
	    final String header = headers[i];
	    switch (this.accessingMethod) {
	    case FIELD_ACCESSOR:
		this.accessorField(resultBean, header, result);
		break;
	    case METHOD_ACCESSOR:
		this.accessorMethod(resultBean, header, result);
		break;
	    case CUSTOM_ACCESSOR:
		this.fieldSet.put(header, result);
		break;
	    default:
		CSVParser.LOG.error("Illegal accessingmethod:" + this.accessingMethod);
		break;
	    }
	}
    }

    private <T> void accessorField(final T resultBean, final String header, final Object result) {
	try {
	    final Field[] fields = this.clazz.getDeclaredFields();
	    for (final Field field : fields) {
		if (field.getName().equalsIgnoreCase(header)) {
		    field.setAccessible(true);
		    field.set(resultBean, result);
		}
	    }
	} catch (final IllegalArgumentException e) {
	    CSVParser.LOG.error(e.getMessage());
	} catch (final IllegalAccessException e) {
	    CSVParser.LOG.error(e.getMessage());
	}
    }

    private <T> void accessorMethod(final T resultBean, final String header, final Object result) {

	try {
	    final Method[] methods = this.clazz.getDeclaredMethods();
	    for (final Method method : methods) {
		if (method.getName().equalsIgnoreCase("set" + header)) {
		    method.setAccessible(true);
		    method.invoke(resultBean, result);

		}
	    }
	} catch (final IllegalArgumentException e) {
	    CSVParser.LOG.error(e.getMessage());
	} catch (final IllegalAccessException e) {
	    CSVParser.LOG.error(e.getMessage());
	} catch (final InvocationTargetException e) {
	    CSVParser.LOG.error(e.getMessage());
	}

    }

    private String[] getHeaders() {
	if (this.headers == null) {
	    try {
		final String line = this.reader.readLine();
		this.headers = line.split(this.delimitor + CSVParser.CSV_REGEX);
	    } catch (final IOException e) {
		CSVParser.LOG.error(e.getMessage());
	    }
	}
	final String[] result = new String[this.headers.length];
	System.arraycopy(this.headers, 0, result, 0, this.headers.length);
	return result;

    }

    public void setMapper(final FieldSetMapper<T> mapper) {
	this.mapper = mapper;
    }

}
